package edu.wpi.cs509.serverlogic;

import java.util.List;

import edu.wpi.cs509.messages.client.MsgGameOver;
import edu.wpi.cs509.messages.client.MsgGameStarted;
import edu.wpi.cs509.messages.client.MsgGameUpdate;
import edu.wpi.cs509.messages.client.MsgJoinGameReply;
import edu.wpi.cs509.messages.client.MsgLeaderBrdReply;
import edu.wpi.cs509.messages.client.MsgLoginReply;
import edu.wpi.cs509.messages.client.MsgOpponentJoined;
import edu.wpi.cs509.messages.client.MsgOpponentReady;
import edu.wpi.cs509.messages.client.MsgStatsReply;
import edu.wpi.cs509.messages.server.MsgServerUserIntfc;
import edu.wpi.cs509.network.Connection;
import edu.wpi.cs509.network.ConnectionListener;
import edu.wpi.cs509.network.ConnectionOwner;
import edu.wpi.cs509.representation.ActionBaseClass;
import edu.wpi.cs509.representation.ActionEndOfGameException;
import edu.wpi.cs509.representation.ActionNotLegalException;
import edu.wpi.cs509.representation.ClientGame;
import edu.wpi.cs509.representation.Game;
import edu.wpi.cs509.representation.GameID;
import edu.wpi.cs509.representation.GameListItem;
import edu.wpi.cs509.representation.TeamColor;
import edu.wpi.cs509.representation.UserName;
import edu.wpi.cs509.representation.UserPair;
import edu.wpi.cs509.representation.UserStatistics;

/**
 * This class contains the main method for the server. It will start the Lobby
 * and UserDB classes and finally kick off the network listener.
 * 
 * @author abrocket
 * 
 */
public class Server implements Runnable, ConnectionOwner, MsgServerUserIntfc {
	ConnectionListener connectionListener = new ConnectionListener(this);
	private Lobby lobby = new Lobby();
	private UserDB userdb = new UserDB();
	private UserStats userStats = new UserStats();
	
	// Private constructor to prevent instantiation outside of getInstance()
	private Server() {
		System.out.println("Server has initialized");
	}
	
	/** 
	 * ServerHolder is loaded on the first execution of Server.getInstance()
	 * This is a way to implement the Singleton pattern
	 * @ref  http://en.wikipedia.org/wiki/Singleton_pattern#The_solution_of_Bill_Pugh
	 */
	private static class ServerHolder {
		public static final Server instance = new Server();
	}
	
	/**
	 * Returns the single instance of Server
	 * @return returns single instance of Server
	 */
	public static Server getInstance() {
		return ServerHolder.instance;
	}
	
	public void run() {
		System.out.println("Begin listening");
		
		// Start the network listener
		connectionListener.run();
	}
	
	/**
	 * @return The user database.
	 */
	public UserDB getUserDB() {
		return userdb;
	}
	
	/**
	 * Returns the game object corresponding to a given game id
	 * 
	 * @param gid - Game ID
	 * @return Reference to game object
	 */
	public Game lookupGame(long gid) {
		return lobby.lookupGame(gid);
	}
	
	/**
	 * Adds a new game object to the Lobby.
	 * @param g - Game to be added
	 */
	public void addGame(Game g) {
		lobby.addGame(g);
	}

	/**
	 * Called when the network connection is terminated.
	 * 
	 *  * @param connection - connection of the user that terminated
	 */
	@Override
	public void handleDisconnect(Connection connection) {
		// Check all games for this connection playing or observing
		lobby.checkForPlaying(connection, true);
		lobby.checkForObserving(connection);
		
		//Update Quit games stat
		userStats.incrementGamesQuit(connection.getUser());
	}
	
	@Override
	public MsgLoginReply.LoginResult login(UserPair pair) {
		return getUserDB().loginUser(pair);
	}
	
	/**
	 * Catch for when the user logs out of the system
	 * 
	 * @param conn - connection of the user that is leaving
	 */
	@Override
	public void logout(Connection conn) {
		// Check all games for this connection playing or observing
		lobby.checkForPlaying(conn, false);
		lobby.checkForObserving(conn);
	}
	
	/**
	 * returns the leaderboard to the client based on the input rank
	 * 
	 * @param rank - integer of rank to search for
	 * @param count - number of users to return
	 * @param conn - connection to the incoming message used for the return message
	 * 
	 * @return the leaderboard as a List<UserStatistics>
	 */
	@Override
	public List<UserStatistics> getLeaderBoard(int rank, int count, Connection conn) {
		try {
			List<UserStatistics> leaderList = userStats.getLeaderBoard(rank, count);
			
			MsgLeaderBrdReply leaderBoardMsg = new MsgLeaderBrdReply(leaderList, rank);
			conn.sendMessage(leaderBoardMsg);
			
			return leaderList;
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}

	/**
	 * return the leaderboard to the client based on a username
	 * 
	 * @param username - username to check off of
	 * @param count - number of entries in the leaderboard
	 * @param conn - connection to the incoming message used for the return message
	 *  
	 * @return the leaderboard as a List<UserStatistics>
	 */
	@Override
	public List<UserStatistics> getLeaderBoard(UserName username, int count, Connection conn) {
		try {
			MsgLeaderBrdReply leaderBoardMsg;
			List<UserStatistics> leaderList;
			
			if( null != username ) {
				leaderList = userStats.getLeaderBoard(username, count);
				leaderBoardMsg = new MsgLeaderBrdReply(leaderList, userStats.getRankFromUsername(username));
			}
			else {
				leaderList = userStats.getLeaderBoard(0, count);
				leaderBoardMsg = new MsgLeaderBrdReply(leaderList, 0);
			}

			
			conn.sendMessage(leaderBoardMsg);
			
			return leaderList;
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}

	/**
	 * returns the indivdual users statistics
	 * 
	 * @param username - username of the user whose stats are being looked up
	 * @param conn - connection to the incoming message used for the return message
	 * 
	 * @return the users stats as a UserStatistics object
	 */
	@Override
	public UserStatistics getPersonalStats(UserName username, Connection conn) {
		System.out.println("Server.java calling userStats.getPersonalStats with name: "+username);
		UserStatistics stats = userStats.getPersonalStats(username);
		int rank = userStats.getRankFromUsername(username);
		
		MsgStatsReply userStatsMsg = new MsgStatsReply(stats,rank);
		conn.sendMessage(userStatsMsg);
		
		return null;
	}

	@Override
	public List<GameListItem> getJoinableGames() {
		return lobby.getList();
	}

	@Override
	public ClientGame startNewGame(Connection playerOne) {
		// Create the new Game
		Game g = new Game();
		TeamColor color = g.addPlayer(playerOne);
		
		// Add that game to the lobby
		lobby.addGame(g);
		
		// Return the representation to the game
		return new ClientGame("", g, color);
	}

	@Override
	public ClientGame joinGame(GameID gId, Connection conn) {
		TeamColor color;
		
		// Attempt to join the game; will return the team color assigned
		// to this player, or null if join fails
		color = lobby.joinGame(gId, conn);
		if (color != null) {
			Game g = lobby.lookupGame(gId.getGameIDLong());

			// notify the joining player
			ClientGame cGame = new ClientGame("", g, color);
			MsgJoinGameReply gameJoinedReply = new MsgJoinGameReply( cGame);
			conn.sendMessage( gameJoinedReply );
			
			// notify the player1 opponent
			TeamColor oppColor = TeamColor.getOppositeColor(color);
			ClientGame oppGame = new ClientGame("", g, oppColor);
			MsgOpponentJoined oppJoinedMsg = new MsgOpponentJoined( oppGame );
			Connection opponent = g.getPlayer(oppColor);
			opponent.sendMessage(oppJoinedMsg);
		}
		return null;
	}
	
	@Override
	public boolean leaveGame(GameID gId, Connection conn) {
		System.out.println("Player " + conn + " is leaving game " + gId);
		return lobby.leaveGame(gId, conn);
	}

	@Override
	public ClientGame observeGame(GameID gId, Connection conn) {
		if (lobby.observeGame(gId, conn)) {
			ClientGame clGame = null;
			Game g = lobby.lookupGame(gId.getGameIDLong());
			clGame = new ClientGame("", g, TeamColor.RED);
			clGame.setObserverGame(true);
			
			//Update the #of games watched stat
			userStats.incrementGamesWatched(conn.getUser());
			return clGame;
		}
		return null;
	}

	@Override
	public List<GameListItem> getObservableGames() {
		return null;
	}

	@Override
	public boolean stopObservingGame(GameID gId, Connection conn) {
		return lobby.stopObservingGame(gId, conn);
	}

	/**
	 * playerReady is invoked when the player has committed all their initial
	 * piece placement.  Once both sides has indicated 'ready', the server will
	 * notify the red player it's their turn to start the game.
	 * @param gId - the ID of the game
	 * @param conn - the connection object for the player
	 */
	@Override
	public boolean playerReady(GameID gId, Connection conn) {

		Game game = lobby.lookupGame(gId.getGameIDLong());
		
		// must have a valid player
		if (conn == null) {
			System.out.println("playerReady passed a null connection argument");
			return false;
		}
		
		// update the game to mark player ready.  If this is the second 
		// player to ready-up, the game will automatically set whoNext
		// to the red player
		game.setDonePlacing(conn);
		
		// Notify the opponent the player is ready
		Connection redPlayer = game.getPlayer(TeamColor.RED);
		Connection bluePlayer = game.getPlayer(TeamColor.BLUE);
		
		if (conn == redPlayer) {
			// notify blueplayer that redplayer is ready
			if (bluePlayer != null) {
				ClientGame blueGame = new ClientGame("", game, TeamColor.BLUE);
				bluePlayer.sendMessage( new MsgOpponentReady(blueGame) );
			}
		} else	if (conn == bluePlayer) {
			// notify redplayer that blueplayer is ready
			if (redPlayer != null) {
				ClientGame redGame = new ClientGame("", game, TeamColor.RED);
				redPlayer.sendMessage( new MsgOpponentReady(redGame) );
			}
		}

		// check if both players have readied-up and tell RED to move
		if ((redPlayer != null && !game.isPlacing(redPlayer)) &&
			(bluePlayer != null && !game.isPlacing(bluePlayer))) {
			
			ClientGame redGame = new ClientGame("", game, TeamColor.RED);
			ClientGame blueGame = new ClientGame("", game, TeamColor.BLUE);
			
			// inform both sides the game is starting
			redPlayer.sendMessage( new MsgGameStarted(redGame) );
			bluePlayer.sendMessage( new MsgGameStarted(blueGame) );
			
		}
		
		// ack the original player ready message
		return true;
	}

	@Override
	public void movePiece(GameID gId, ActionBaseClass moveAction, Connection conn) {
		
		Game game = lobby.lookupGame(gId.getGameIDLong());
		
		String error = null;
		ActionEndOfGameException gameOver = null;
		
		Connection redPlayer = game.getPlayer(TeamColor.RED);
		Connection bluePlayer = game.getPlayer(TeamColor.BLUE);

		try {
			System.out.println("in server.movePiece... server gameboard before Action.execute()");
			System.out.println(game.getGameboard().toString());

			if(!game.isGameStarted() && !game.isPlacing(conn)) {
				ClientGame g = new ClientGame("", game, game.getColor(conn));
				//Send back untouched game board because they are trying to place pieces when they are already readied
				conn.sendMessage(new MsgGameUpdate(g, moveAction, game.getColor(conn), false, "Can't place pieces, you are readied up"));
				return;
			} else {
				moveAction.setGameboard(game.getGameboard());
				moveAction.execute();
				System.out.println("======the moveAction (after execute) ======"+moveAction.toString());
			}
			
			// if the move executed without error, we change whose turn it is
			game.changeTurn();
			
			System.out.println("in server.movePiece... server gameboard after Action.execute()");
			System.out.println(game.getGameboard().toString());

		} catch (ActionNotLegalException e) {
			error = "Action not legal";
		} catch (ActionEndOfGameException e) {
			// the game has ended.  This exception holds the information about the winner.
			gameOver = e;
		}

		// If the red player exists, send them an update
		if (redPlayer != null) {
			ClientGame redGame = new ClientGame("", game, TeamColor.RED);
			
			// If there was an error, and red initiated the move, send error message only to red
			if ((error != null) && (conn == redPlayer)) {
				conn.sendMessage(new MsgGameUpdate(redGame, moveAction, TeamColor.RED, false, error));
				return;
			} 							

			// If there was no error, send red a move update
			if (error == null) {
				redPlayer.sendMessage(new MsgGameUpdate(redGame, moveAction, game.getWhoseTurn()));
				
				if (gameOver != null) {
					redPlayer.sendMessage( new MsgGameOver(redGame, moveAction,  gameOver.redWins()) );
					
					//Update the the game players stats
					userStats.updateGamesWon(redPlayer.getUser());
					userStats.updateGamesLost(bluePlayer.getUser());
				}
			}

			// note if there was an error, but blue initiated the move, no update is sent to red			
		}
		
		
		// If the blue player exists, send them an update
		if (bluePlayer != null) {
			ClientGame blueGame = new ClientGame("", game, TeamColor.BLUE);

			// If there was an error, and blue initiated the move, send error message only to blue
			if ((error != null) && (conn == bluePlayer)) {
				conn.sendMessage(new MsgGameUpdate(blueGame, moveAction, TeamColor.BLUE, false, error));
				return;
			} 

			// If there was no error, send blue a move update
			if (error == null) {
				bluePlayer.sendMessage(new MsgGameUpdate(blueGame, moveAction, game.getWhoseTurn()));
				
				if (gameOver != null) {
					bluePlayer.sendMessage( new MsgGameOver(blueGame, moveAction,  gameOver.blueWins()) );
					
					//Update the the game players stats
					userStats.updateGamesWon(bluePlayer.getUser());
					userStats.updateGamesLost(redPlayer.getUser());
				}
			}

			// note if there was an error, but red initiated the move, no update is sent to blue
			
		}
		
		ClientGame observerGame = new ClientGame("", game, TeamColor.BOTH); 
		
		// send move update to all observers
		for (Connection c: game.getObservers()) {
			c.sendMessage(new MsgGameUpdate(observerGame, moveAction, TeamColor.BOTH, true, ""));
		}

	}

}
